package com.kevin.common.util;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.kevin.common.CacheConstant;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.CollectionUtils;

import java.io.Serializable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 封装redis的操作
 *
 * @author wh
 * @date 2021-05-31
 */
@Slf4j
public class RedisUtil {
    /**
     * redisTemplate
     */
    @SuppressWarnings("unchecked")
    // TODO:这边用RedisTemplateClass获取的话 会出现  expected single matching bean but found 2: redisTemplate,stringRedisTemplate
    private static RedisTemplate<Serializable, Object> redisTemplate = SpringUtil.getBean("redisTemplate");
    private static StringRedisTemplate stringRedisTemplate = SpringUtil.getBean(StringRedisTemplate.class);

    /**
     * Jackson Mapper
     */
    private static ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public static boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            log.info("[redisTemplate redis]放入 缓存  url:{}", key);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 更新值,不修改失效时间
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public static boolean update(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value, 0);
            log.info("[redisTemplate redis]更新 缓存  url:{}", key);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public static boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            log.info("[redisTemplate redis]放入 缓存  url:{} ========缓存时间为:{}秒", key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 读取缓存
     */
    public static <T> T getJsonObject(final String key, Class<T> clazz) {
        Object result;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        log.info("[redisTemplate redis]取出 缓存  url:{} ", key);
        return result == null ? null : objectMapper.convertValue(result, clazz);
    }

    /**
     * 读取缓存
     */
    public static <T> T getJsonObject(final String key, TypeReference<T> typeReference) {
        Object result;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        log.info("[redisTemplate redis]取出 缓存  url:{} ", key);
        return objectMapper.convertValue(result, typeReference);
    }


    /**
     * 读取缓存
     */
    public static <T> T get(final String key, Class<T> clazz) {
        Object result;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        log.info("[redisTemplate redis]取出 缓存  url:{} ", key);
        return result == null ? null : objectMapper.convertValue(result, clazz);
    }

    /**
     * 读取缓存
     */
    public static String get(final String key) {
        Object result;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        log.info("[redisTemplate redis]取出 缓存  url:{} ", key);
        return result == null ? null : result.toString();
    }

    private static final String LUA_INCR_EXPIRE =
            "local key,ttl=KEYS[1],ARGV[1] \n" +
                    " \n" +
                    "if redis.call('EXISTS',key)==0 then   \n" +
                    "  redis.call('SETEX',key,ttl,1) \n" +
                    "  return 1 \n" +
                    "else \n" +
                    "  return tonumber(redis.call('INCR',key)) \n" +
                    "end ";

    // https://www.cnblogs.com/redwinter/p/16023853.html 必须使用StringRedisTemplate
    public static Long inc(String key, int time, TimeUnit unit) {
        RedisScript<Long> redisScript = new DefaultRedisScript<>(LUA_INCR_EXPIRE, Long.class);
        return stringRedisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(unit.toSeconds(time)));
    }

    public static <T> List<T> mget(Collection<Serializable> keys, Class<T> tClass) {
        List<Object> list = redisTemplate.opsForValue().multiGet(keys);
        return list == null ? new ArrayList<>() : list.stream().map(o -> objectMapper.convertValue(o, tClass)).collect(Collectors.toList());
    }

    public static void hSet(String key, String field, Object value) {
        hSet(key, field, value, CacheConstant.DEFAULT_EXPIRE);
    }

    /**
     * 设置缓存hash
     */
    public static void hSet(final String key, String filed, Object value, long time) {
        HashOperations<Serializable, Object, Object> operations = redisTemplate.opsForHash();
        operations.put(key, filed, value);
        if (time > 0) {
            // 设置失效时间
            expire(key, time);
        }
        log.info("[redisTemplate redis]设置 hash缓存  map url:{} 失效时间:{} ", key, time);
    }

    /**
     * 设置缓存hash
     */
    public static void hmSet(final String key, Map<?, ?> map, long time) {
        HashOperations<Serializable, Object, Object> operations = redisTemplate.opsForHash();
        operations.putAll(key, map);
        if (time > 0) {
            // 设置失效时间
            expire(key, time);
        }
        log.info("[redisTemplate redis]设置 hash缓存  map url:{} 失效时间:{} ", key, time);
    }

    /**
     * 读取缓存
     */
    public static Object hget(final String key, final String item) {
        Object result;
        HashOperations<Serializable, Object, Object> operations = redisTemplate.opsForHash();
        result = operations.get(key, item);
//        log.info("[redisTemplate redis]取出 缓存  map url:{} {} ", key, item);
        return result;
    }

    public static void hDel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 读取缓存
     */
    public static Map<String, Object> hmGet(final String key, List<String> item) {
        HashOperations<Serializable, Object, Object> operations = redisTemplate.opsForHash();
        List<Object> itemOList = item.stream().distinct().filter(StrUtil::isNotEmpty).map(e -> (Object) e).collect(Collectors.toList());
        List<Object> objects = operations.multiGet(key, itemOList);
        Map<String, Object> result = new HashMap<>();
        for (int i = 0; i < itemOList.size(); i++) {
            result.put((String) itemOList.get(i), objects.get(i));
        }
        return result;
    }

    /**
     * 读取缓存
     */
    public static Map<String, Object> hmGetAll(final String key) {
        HashOperations<Serializable, Object, Object> operations = redisTemplate.opsForHash();
        Map<Object, Object> entries = operations.entries(key);
        Map<String, Object> result = new HashMap<>();
        entries.forEach((k, v) -> {
            result.put(k + "", v);
        });
        return result;
    }

    /**
     * 指定缓存失效时间
     */
    public static void expire(final String key, long time) {
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
        log.info("[redisTemplate redis]设置缓存失效时间  url:{} ", key);
    }

    /**
     * 是否存在
     */
    public static Boolean exist(final String key) {
        Boolean exist = redisTemplate.hasKey(key);
//        log.info("[redisTemplate redis]缓存是否存在  url:{} result:{}", key, exist);
        return exist;
    }

    /**
     * 读取缓存
     */
    public static Long ttl(final String key) {
        Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        log.info("[redisTemplate redis]取出 缓存时长  url:{} ========剩余缓存时间为:{}秒", key, expire);
        return expire;
    }

    /**
     * 删除缓存
     */
    public static long del(final Collection collection) {
        Long result = redisTemplate.delete(collection);
        log.info("[redisTemplate redis]删除缓存 缓存时长  url:{} ", collection);
        return result;
    }

    /**
     * 删除缓存
     */
    public static void del(final String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Serializable) CollectionUtils.arrayToList(key));
            }
        }
        log.info("[redisTemplate redis]删除缓存 缓存时长  url:{} ", key);
    }

    /**
     * 读取缓存
     * 不可以用 keys https://blog.csdn.net/oschina_40914891/article/details/100151227
     */
    /**
     * 查找匹配key
     *
     * @param pattern key
     * @return /
     */
    public static List<String> scan(final String pattern) {
        ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection rc = Objects.requireNonNull(factory).getConnection();
        Cursor<byte[]> cursor = rc.scan(options);
        List<String> result = new ArrayList<>();
        while (cursor.hasNext()) {
            result.add(new String(cursor.next()));
        }
        try {
            RedisConnectionUtils.releaseConnection(rc, factory);
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("[redisTemplate redis]查询keys  pattern:{} result-size:{}", pattern, result.size());
        return result;
    }

    /**
     * 解锁
     *
     * @param key   redis主键
     * @param value 值
     */
    public static boolean unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(CacheConstant.LOCK_KEY + key), value);
        if (Objects.equals(1L, result)) {
            log.info("[redisTemplate redis]释放锁 缓存  url:{}", key);
            return true;
        }
        return false;
    }

    /**
     * 加锁
     *
     * @param key   redis主键
     * @param value 值
     * @param time  过期时间
     */
    public static boolean lock(String key, String value, long time) {
        final boolean result = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(CacheConstant.LOCK_KEY + key, value, time, TimeUnit.SECONDS));
        if (result) {
            log.info("[redisTemplate redis]设置锁缓存 缓存  url:{} ========缓存时间为{}秒", key, time);
        }
        return result;
    }


    /*******下面后续优化**/
    public static Boolean lockEnable(String key, long expire) {

        //这是将当前线程的名字置为key的value值,表明该锁被谁拿到
        String keyValue = Thread.currentThread().getName();

        //1,这是StringRedisTemplate在set key的同时增加了过期时间，防止死锁。保证了原子性。
        //2,setIfAbsent该方法如果该key不存在时候，设置值进去后，返回true;若是已经存在，则返回false;
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, keyValue, expire, TimeUnit.SECONDS);
        Long surplusTime = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        if (!aBoolean) {
            log.info("该线程【{}】加锁失败,该key【{}】剩余过期时间【{}】秒", keyValue, key, surplusTime);
            return false;
        }
        log.info("该线程【{}】加锁成功,该key【{}】剩余过期时间【{}】秒", keyValue, key, surplusTime);
        return true;
    }

    public static Boolean lockUnable(String key) {
        String keyValue = Thread.currentThread().getName();
        //key和value不一致时，返回：【0】
        //key和value不一致时，返回：【1】
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long execute = redisTemplate.execute(redisScript, Arrays.asList(key, keyValue));
        if (execute != 1) {
            Boolean aBoolean = redisTemplate.hasKey(key);
            Long surplusTime = redisTemplate.getExpire(key, TimeUnit.SECONDS);
            log.info("该key【{}】解锁失败,是否存在【{}】,剩余过期时间【{}】秒", key, aBoolean, surplusTime);
            return false;
        }
        log.info("该key【{}】解锁成功", key);
        Boolean aBoolean = redisTemplate.hasKey(key);
        log.info("该key是否存在【{}】", aBoolean);
        return true;
    }


}
